use std::{path::Path, sync::Arc};

use secp256k1::rand;
use tokio::sync::Mutex;
use tracing::{debug, error, info, trace};

use bc_base::{
    PrivateKeyBytes, PublicKeyBytes, PublicKeyHashBytes, TransactionId, TransactionValue,
    block::Block,
    transaction::{Transaction, TransactionOut},
    wallet::{ItemNumberInWalletDb, wallet_cache::WalletCache, wallet_tx::WalletTx},
};
use bc_block_data::WalletStorage;
use bc_consensus::Consensus;

use crate::WalletsError;

#[derive(Debug)]
pub(crate) struct WalletStorageWithCache {
    wallet_cache: WalletCache,
    wallet_storage: WalletStorage,
}

impl WalletStorageWithCache {
    pub(crate) async fn new(db_root_dir: &Path) -> Self {
        let path = bc_block_data::get_wallet_db_path(db_root_dir);
        let mut t = Self {
            wallet_cache: WalletCache::new(),
            wallet_storage: WalletStorage::new(&path).unwrap(),
        };

        bc_util::execute_function_elapsed("Load wallet", async {
            let wallet_cache = WalletCache::new();
            t.wallet_cache = t.wallet_storage.load_to_wallet_cache(wallet_cache).await;
            info!(
                "wallet txs that in cache cnt: {}. DB path: {}",
                t.wallet_cache.get_wallet_txs().len(),
                path.to_str().unwrap(),
            );
        })
        .await;

        t
    }

    pub(crate) async fn get_item_number(&self) -> ItemNumberInWalletDb {
        let n1 = self.wallet_cache.get_item_number();
        #[cfg(debug_assertions)]
        {
            let n2 = self.wallet_storage.get_item_number().await;
            debug_assert_eq!(n1, n2);
        }
        n1
    }

    pub(crate) fn get_wallet_cache(&self) -> &WalletCache {
        &self.wallet_cache
    }
}

// for account
impl WalletStorageWithCache {
    // For function 1: it generates private keys, derives the corresponding public keys, helps distribute those public keys as necessary,
    pub(crate) async fn create_account(&mut self) -> Result<PublicKeyHashBytes, WalletsError> {
        let (private_key, public_key) = bc_util::generate_secp256k1_keypair();
        let public_key = PublicKeyBytes::new(public_key.serialize());
        let private_key = PrivateKeyBytes::new(private_key.secret_bytes());
        let pub_key_hash = self.wallet_cache.add_keypair(&public_key, &private_key)?;
        self.wallet_storage
            .put_keypair_to_wallet_db(&public_key, &private_key)
            .await?;
        Ok(pub_key_hash)
    }

    pub(crate) fn exists_account(&self, public_key_hash: &PublicKeyHashBytes) -> bool {
        self.wallet_cache.is_mine(public_key_hash)
    }

    pub(crate) fn exists_account_1(&self, public_key: &PublicKeyBytes) -> bool {
        self.wallet_cache.is_mine_1(public_key)
    }

    pub(crate) async fn add_account(
        &mut self,
        public_key: &PublicKeyBytes,
        private_key: &PrivateKeyBytes,
    ) -> Result<(), WalletsError> {
        let _ = self.wallet_cache.add_keypair(public_key, private_key)?;
        self.wallet_storage
            .put_keypair_to_wallet_db(public_key, private_key)
            .await?;
        Ok(())
    }

    // pub(crate) async fn del_account(&mut self, public_key: &PublicKeyBytes) -> Result<(), WalletsError> {
    //     self.wallet_cache.del_account(public_key)?;
    //     self.wallet_storage
    //         .del_account_from_wallet_db(public_key)
    //         .await?;
    //     Ok(())
    // }
}

impl WalletStorageWithCache {
    // For function 2: monitors for outputs spent to those public keys,
    pub(crate) async fn add_tx_if_mine(
        &mut self,
        tx_id: &TransactionId,
        tx: &Transaction,
    ) -> Result<(), WalletsError> {
        trace!("Add tx if mine: {}", tx_id);
        if self.is_mine(tx) {
            trace!("Add tx({}) to wallet.", tx_id);
            self.wallet_cache.add_tx_to_wallet(tx_id, tx);
            self.wallet_storage.put_tx_to_wallet_db(tx_id, tx).await?;
        }

        Ok(())
    }

    pub(crate) fn get_my_pub_key_hash_from_tx_out(
        &self,
        tx_out: &TransactionOut,
    ) -> Result<Option<PublicKeyHashBytes>, WalletsError> {
        let pub_key_hash = tx_out.get_payee_pubkey_hash()?;

        if self.wallet_cache.is_mine(&pub_key_hash) {
            Ok(Some(pub_key_hash))
        } else {
            Ok(None)
        }
    }
    fn is_mine(&self, tx: &Transaction) -> bool {
        let mut r = false;
        for tx_out in tx.get_outputs() {
            let pub_key_hash = tx_out.get_payee_pubkey_hash().unwrap();
            if self.wallet_cache.is_mine(&pub_key_hash) {
                r = true;
                break;
            }
        }

        r
    }

    pub(crate) fn is_tx_in_wallet(&self, tx_id: &TransactionId) -> bool {
        self.wallet_cache.is_tx_in_wallet(tx_id)
    }

    // bool CommitTransaction(CWalletTx& wtxNew, const CKey& key) {
    pub(crate) async fn commit_transaction(
        &mut self,
        tx: &Transaction,
    ) -> Result<(), WalletsError> {
        let tx_id = tx.calc_tx_id();
        self.add_tx_if_mine(&tx_id, tx).await?;

        let transaction_in = tx.get_inputs();
        for tx_in in transaction_in {
            let prev_tx_id = tx_in.get_prev_out_point().get_tx_id();
            if let Some(wtx) = self.wallet_cache.get_wallet_tx(prev_tx_id) {
                let mut wtx = wtx.clone();
                wtx.set_spent();

                self.wallet_storage
                    .put_wallet_tx_to_wallet_db(prev_tx_id, &wtx)
                    .await?;
                self.wallet_cache.add_wallet_tx(prev_tx_id, wtx);
            } else {
                error!("Transaction not found in wallet when trying to spend it");
            }
        }
        Ok(())
    }

    pub(crate) async fn add_my_txs_in_block(&mut self, block: &Block) -> Result<(), WalletsError> {
        let merkle_tree = block.get_merkle_tree();
        let transactions = block.get_transactions();
        assert!(transactions.len() == 1 || merkle_tree.len() > transactions.len()); // transactions.len() == 1 --> only contain coinbase tx
        for (index, tx) in transactions.iter().enumerate() {
            let tx_id = &merkle_tree[index];
            self.add_tx_if_mine(tx_id, tx).await?;
        }

        trace!("Add my txs to my utxo set done.");

        Ok(())
    }
}

impl WalletStorageWithCache {
    pub(crate) fn get_pub_key(
        &self,
        pub_key_hash: &PublicKeyHashBytes,
    ) -> Result<PublicKeyBytes, WalletsError> {
        let t = self.wallet_cache.get_pub_key(pub_key_hash)?;
        Ok(t)
    }

    pub(crate) fn get_private_key(
        &self,
        public_key: &PublicKeyBytes,
    ) -> Result<PrivateKeyBytes, WalletsError> {
        let t = self.wallet_cache.get_private_key(public_key)?;
        Ok(t)
    }

    pub(crate) async fn store(&self) -> Result<(), WalletsError> {
        debug!("Storing to wallet db...");
        self.wallet_storage.store_wallet(&self.wallet_cache).await?;
        Ok(())
    }

    pub(crate) async fn flush_database(&self) -> Result<(), WalletsError> {
        debug!("Flushing database of wallet...");
        self.wallet_storage.flush_database().await?;
        Ok(())
    }

    pub(crate) fn dump_wallet_account(&self) {
        self.wallet_cache.dump_accounts();
    }
}

// For function 3: creates and signs transactions spending those outputs, and broadcasts the signed transactions.
// select_coins
impl WalletStorageWithCache {
    async fn filter_and_categorize_transactions(
        &self,
        target_value: TransactionValue,
        best_hight: u32,
        consensus: Arc<Mutex<Consensus>>,
    ) -> (
        Vec<WalletTx>,
        TransactionValue,
        Vec<(TransactionValue, &WalletTx)>,
        TransactionValue,
        Option<WalletTx>,
    ) {
        let mut coins_ret = Vec::new();

        let mut lower_total_value: TransactionValue = 0;
        let mut lower_txs: Vec<(TransactionValue, &WalletTx)> = Vec::new();

        let mut lowest_larger_value: TransactionValue = TransactionValue::MAX;
        let mut lowest_larger_tx: Option<WalletTx> = None;

        for (tx_id, wtx) in self.wallet_cache.get_wallet_txs().iter() {
            let block_time = if let Some(_blk_id) = wtx.get_belong_to_block() {
                // TODO: get block time from block index
                0
            } else {
                consensus.lock().await.get_adjusted_timestamp().await as i64
            };

            if !wtx.is_final(block_time, best_hight) || wtx.is_spent() {
                continue;
            }

            let n = crate::get_credit(wtx.get_tx(), &self.wallet_cache, consensus.clone()).await;
            if n <= 0 {
                error!("Wallet tx({:?}) credit is negative", tx_id);
                continue;
            }

            if n < target_value {
                lower_total_value += n;
                lower_txs.push((n, wtx));
            } else if n == target_value {
                coins_ret.push(wtx.clone());
                break;
            } else if n < lowest_larger_value {
                lowest_larger_value = n;
                lowest_larger_tx = Some(wtx.clone());
            } else {
                continue;
            }
        }

        (
            coins_ret,
            lower_total_value,
            lower_txs,
            lowest_larger_value,
            lowest_larger_tx,
        )
    }

    fn solve_subset_sum<'a>(
        &'a self,
        target_value: TransactionValue,
        lower_total_value: TransactionValue,
        mut lower_txs: Vec<(TransactionValue, &'a WalletTx)>,
    ) -> (
        TransactionValue,
        Vec<bool>,
        Vec<(TransactionValue, &'a WalletTx)>,
    ) {
        lower_txs.sort_by(|a, b| b.0.cmp(&a.0));
        let lower_txs_cnt = lower_txs.len();
        let mut f_included = vec![false; lower_txs_cnt];

        let mut f_best = vec![false; lower_txs_cnt];
        let mut n_best = lower_total_value;
        for _ in 0..1000 {
            let mut n_total = 0;
            let mut f_reached_target = false;

            for n_pass in 0..2 {
                for i in 0..lower_txs_cnt {
                    let flag = if n_pass == 0 {
                        rand::random::<bool>()
                    } else {
                        !f_included[i]
                    };

                    if !flag {
                        continue;
                    }

                    n_total += lower_txs[i].0;
                    f_included[i] = true;

                    if n_total >= target_value {
                        f_reached_target = true;

                        if n_total < n_best {
                            n_best = n_total;
                            f_best = f_included.clone();
                        }

                        n_total -= lower_txs[i].0;
                        f_included[i] = false;
                    }
                }

                if f_reached_target {
                    break;
                }
            }

            if n_best == target_value {
                break;
            }

            f_included.fill(false);
        }

        (n_best, f_best, lower_txs)
    }

    pub(crate) async fn select_coins(
        &self,
        target_value: TransactionValue,
        best_hight: u32,
        consensus: Arc<Mutex<Consensus>>,
    ) -> Result<Vec<WalletTx>, WalletsError> {
        let (mut coins_ret, lower_total_value, lower_txs, lowest_larger_value, lowest_larger_tx) =
            self.filter_and_categorize_transactions(target_value, best_hight, consensus)
                .await;
        if !coins_ret.is_empty() {
            return Ok(coins_ret);
        }

        if lower_total_value < target_value {
            if let Some(tx) = lowest_larger_tx {
                coins_ret.push(tx);
                return Ok(coins_ret);
            } else {
                return Err(WalletsError::InsufficientBalance);
            }
        }

        // Solve subset sum by stochastic approximation
        let (n_best, f_best, lower_txs) =
            self.solve_subset_sum(target_value, lower_total_value, lower_txs);

        // If the next larger is still closer, return it
        let lower_txs_cnt = lower_txs.len();
        let mut pick_lower_txs = || {
            for i in 0..lower_txs_cnt {
                if f_best[i] {
                    coins_ret.push(lower_txs[i].1.clone());
                }
            }
        };

        match lowest_larger_tx {
            Some(tx) => {
                if lowest_larger_value - target_value <= n_best - target_value {
                    coins_ret.push(tx);
                } else {
                    pick_lower_txs();
                }
            }
            None => {
                pick_lower_txs();
            }
        }

        Ok(coins_ret)
    }
}
